class Component {
    constructor() {
        this._events = {};
    }
    fire(type, event, obj) {
        type = type.toLowerCase();
        let handlers = this._events[type];
        if (handlers) {
            if (!event) event = {};
            if (event && event != this) {
                event.sender = event.sender || this;
                if (!event.type) {
                    event.type = type;
                }
            }
            for (let i = 0, l = handlers.length; i < l; i++) {
                let listener = handlers[i];
                let fn=listener.fn, scop=listener.scope, delay=listener.delay, interval=listener.interval;
                let expression=listener.expression, dobj=listener.dobj, iobj=listener.iobj,prtn=listener.prtn;
                if (expression) {
                    let rtn = eval(expression);
                    if (rtn) {
                        if (prtn) continue;
                        if (delay) {
                            if (!dobj) {
                                if (interval) {
                                    dobj = setTimeout(() => {
                                        iobj = setInterval(() => {
                                            fn.call(scop, event);
                                        }, interval);
                                        listener.iobj = iobj;
                                    }, delay);
                                    listener.dobj = dobj;
                                } else {
                                    if(dobj==0){
                                        dobj = setTimeout(() => {
                                            fn.call(scop, event);
                                        }, delay);
                                        listener.dobj = dobj;
                                    }                                    
                                }
                            }
                        } else if (interval) {
                            if (!iobj) {
                                iobj = setInterval(() => {
                                    fn.call(scop, event);
                                }, interval);
                                listener.iobj = iobj;
                            }
                        } else {
                            fn.call(scop, event);
                        }
                    } else {
                        if (dobj) {
                            clearTimeout(dobj);
                            listener.dobj = 0;
                        }
                        if (iobj) {
                            clearInterval(iobj);
                            listener.iobj = 0;
                        }
                    }
                    listener.prtn = rtn;
                } else {
                    fn.call(scop, event);
                }
            }
        }
    }

    on(type, handler){//fn, scope, delay, interval, expression) {
        if (typeof handler.fn != 'function' || !type) return;
        handler.scope = handler.scope || this;
        type = type.toLowerCase();

        let event = this._events[type];
        if (!event) {
            event = this._events[type] = [];
        }
        let fn=handler.fn, scope=handler.scope, delay=handler.delay, interval=handler.interval, expression=handler.expression;
        if (!this.findListener(type, fn, scope, delay, interval, expression)) {
            event.push(handler);
        }
    }

    un(type, fn, scope, delay, interval, expression) {
        if (typeof fn != 'function' || !type) return;
        scope = scope || this;
        delay = delay || 0;
        interval = interval || 0;
        expression = expression || "";
        type = type.toLowerCase();

        let listener = this.findListener(type, fn, scope, delay, interval, expression);
        if (listener) {
            let handlers = this._events[type];
            handlers.remove(listener);
            if (handlers.length == 0) {
                delete this._events[type];
            }
        }
    }
    findListener(type, fn, scope, delay, interval, expression) {
        if (typeof fn != 'function' || !type) return false;
        type = type.toLowerCase();
        var handlers = this._events[type];
        if (handlers) {
            for (var i = 0, l = handlers.length; i < l; i++) {
                var listener = handlers[i];
                if (listener.fn === fn && listener.scope === scope && listener.delay == delay &&
                    listener.interval == interval && listener.expression== expression) return listener;
            }
        }
        return false;
    }
}
class OPCTag extends Component {
    constructor(plc, name, addr) {
        super();
        this._plc = plc;
        this._addr = addr;
        this._name = name;
        this._quality = 0;
        this._timestamp = null;
        this._listener = [];
    };
    get addr() {
        return this._addr;
    }
    get name() {
        return this._name;
    }

    get value() {
        if (this.bufvalue !== undefined) return this.bufvalue;
        return this._value;
    }
    set value(v) {
        if (this._precision && (typeof this._precision == "number")) v = +v.toFixed(this._precision);
        if (this._value != v) {
            this._value = v;
            this.bufvalue = undefined;
            this.notify();
            if (this._plc._tags.plcopened.value) {
                this.fire('change', {
                    value: this._value,
                    tagName: this._name,
                    addr: this._addr
                }, this._plc._tags);
            }
        }
    }

    get timeStamp() {
        return this._timestamp;
    }
    set timeStamp(value) {
        this._timestamp = value;
    }

    get quality() {
        return this._quality;
    }
    set quality(value) {
        this._quality = value;
    }

    notify() {
        for (let i = 0, len = this._listener.length; i < len; i++) {
            let [obj, cmd, prop, org, ext] = this._listener[i];
            let txt;
            try {
                txt = org.match(/^\$.*\$$/) ? eval(org.substring(1, org.length - 1)) :
                    org.replace(/\$.*\$/, (e) => {
                        return eval(e.substring(1, e.length - 1));
                    });
            } catch (err) {
                console.log(err.message);
                continue;
            }

            if (typeof txt == "string") {
                let mc = txt.match(/function\(\) *?\{([\s\S\n]*)\}/);
                if (mc) {
                    eval(mc[1]);
                    continue;
                }
            }
            if (mini.isControl(obj)) {
                if (cmd.match(/style/i)) {
                    obj[cmd](prop + ":" + txt);
                } else {
                    obj[cmd](txt);
                }
            } else {
                if (cmd == 'style') {
                    obj[cmd][prop] = txt;
                } else if (cmd != "") {
                    obj[cmd](prop, txt);
                } else {
                    obj[prop] = txt;
                }
            }
        }
    }
}
class PLC extends Component {
    _decodeMsg(msg) {
        let o = JSON.parse(msg);
        o.forEach(e => {
            let fn = e.name.replace(/\./g, '\_');
            this._tags[fn].value = e.value;
            this._tags[fn].timeStamp = new Date((e.timeStamp - 25569) * 24 * 3600 * 1000);
            this._tags[fn].quality = e.quality;
        });
        return o;
    }
    _registTags(plcTags) {
        let reqs = [];
        for (let i = 0, len = plcTags.length; i < len; i++) {
            let name = plcTags[i][0].toLowerCase();
            let addr = plcTags[i][1];
            let aname = addr.replace(/\./g, '\_');
            let tag = new OPCTag(this, name, addr);
            this._tags[name] = tag;
            if (name != aname && aname)
                this._tags[aname] = tag;
            if (aname) {
                reqs.push(addr);
                if (plcTags[i].length > 2) {
                    this._tags[name]._precision = plcTags[i][2];
                }
            } else
                this._tags[name]._value = plcTags[i][2];
        }
        if (reqs.length == 0) return true;
        let cmd = {
            cmd: 'regist',
            pt: reqs
        };
        return new Promise((resolve, reject) => {
            this._opened.then((e) => {
                let oldmsg = this._ws.onmessage;
                let timeout = setTimeout(() => {
                    reject("Tag Regist TimeOut.Error Tags:" + reqs.join(","));
                    this._ws.onmessage = oldmsg;
                    reqs=undefined;
                    cmd=undefined;
                }, reqs.length * 200);

                this._ws.send(JSON.stringify(cmd));
                this._ws.onmessage = (d) => {
                    let o = this._decodeMsg(d.data);
                    for (let i = 0, len = o.length; i < len; i++) {
                        let idx = reqs.indexOf(o[i].name);
                        if (idx < 0) continue;
                        reqs.splice(idx, 1);
                        if (reqs.length == 0) {
                            clearTimeout(timeout);
                            this._ws.onmessage = oldmsg;
                            reqs=undefined;
                            cmd=undefined;
                            resolve(true);
                            break;
                        }
                    }
                }
            }).catch((result) => {
                reject(result)
            })
        })
    }
    get self() {
        return this._self;
    }
    __writeTag() {
        let reqs = {};
        let me = this;
        let timer;
        return (addr, value) => {
            reqs[addr] = value;
            if (timer) return;
            timer = setTimeout(() => {
                let cmd = {
                    cmd: 'write',
                    pt: reqs
                };
                me._ws.send(JSON.stringify(cmd));
                timer = undefined;
                reqs = {};
            }, 1);
        }
    }
    constructor(ipAddr, plcTags) {
        super();
        this._tags = {};
        this._self = this;
        this._scanRate = 100;
        /**
         * @type {WebSocket}
         */
        this._ws = new WebSocket('ws://' + ipAddr);
        this._opened = new Promise((resolve, reject) => {
            this._ws.onopen = () => {
                resolve(true);
            }
            this._ws.onerror = (e) => {
                reject('Socket Error');
            }
        })
        this._ws.onmessage = (e) => {
            this._decodeMsg(e.data);
        }
        this._writeTag = this.__writeTag();
        let proxy = new Proxy(this, this);;
        this._registTags([
            ["plcopened", "", false]
        ]);
        this._registTags(plcTags).then((v) => {
            for (let [key, value] of this._monitors) {
                this._monitors.delete(key);
                this.addListener(value);
            }
            this.observer.disconnect();
            delete PLC.prototype.observer;
            this._collectListeners(this._monitors);
            this.fire('open', {
                sender: proxy
            });
            try {
                this._tags.plcopened.value = true;
            } catch (e) {
                this.fire("error",{send:proxy,msg:e});
            }     
        }).catch((v) => {
            if (this._ws.readyState == this._ws.OPEN) this._ws.close();
            this._ws.close(1000);
            if (this._events["error"]) {
                this.fire("error", {
                    sender: proxy,
                    msg: v
                })
            } else {
                throw (v);
            }
        });
        return proxy;
    }
    get(obj, prop) {
        let v = obj[prop];
        if (v) return v;

        let a = prop.split('$');
        prop = a[0].toLowerCase();
        v = a[1] || "value"
        let tag = obj._tags[prop];
        if (tag) return tag[v];
        throw 'Propety not found';
    }
    set(obj, prop, value) {
        let v = obj[prop];
        if (v) {
            if (v != value)
                obj[prop] = value;
            return true;
        }
        prop = prop.toLowerCase();
        let tag = obj._tags[prop];
        if (tag) {
            if (tag._value != value) {
                if (tag._addr) {
                    tag.bufvalue = value;
                    obj._writeTag(tag.addr, value);
                } else {
                    tag.value = value;
                }
            }
        } else {
            throw "Propety no found.";
        }
        return true;
    }
    on(type, fn, delay, interval, scope) {
        delay = delay || 0;
        interval = interval || 0;
        if (typeof fn != 'function' || !type) return undefined;

        scope = scope || this;
        let self = this.self;
        let reg1 = /^[a-zA-Z_]\w*$/;
        let reg2 = /([a-zA-Z_]\w*)/g;
        let handler={
            fn:fn,
            scope:scope,
            delay:delay,
            interval:interval,
            expression:"",
            dobj:0,
            iobj:0,
            prtn:false
        }
        if (reg1.test(type)) {
            type = type.toLowerCase();
            let tag = self._tags[type]
            if (!tag) {
                super.on.call(self, type, handler)
            } else {
                tag.on('change', handler);
                self._tags.plcopened.on("change", handler);
            }
            return;
        }
        type = type.replace(reg2, (e) => {
            let tn = e.toLowerCase();
            if (self._tags[tn])
                return "obj." + tn + "._value"
            else
                return e;
        });
        reg2.lastIndex = 0;
        for (let mc; mc = reg2.exec(type);) {
            let s = mc[1]
            let tag = self._tags[s];
            if (!tag) continue;
            handler.expression=type;
            tag.on("change", handler);
            self._tags.plcopened.on("change", handler);
        }
    }
    get scanRate() {
        return this._scanRate;
    }
    set scanRate(value) {
        this._scanRate = value;
        let cmd = JSON.stringify({
            cmd: 'scanrate',
            pt: value
        });
        if (this._ws.readyState == this._ws.OPEN)
            this._ws.send(cmd);
        else
            this._opened.then(
                e=>{
                    this._ws.send(cmd);
                }
            )
    }
    removeListener(tag, listener) {
        let idx = tag._listener.indexOf(listener);
        if (idx >= 0) tag._listener.splice(idx, 1);
    }
    addListener(tagList) {
        let self = this.self;
        tagList.forEach((v) => {
            let reg2 = /([a-zA-Z_]\w*)/g;
            let reg3 = /\$(.*?)\$/g;
            v[3] = v[3].replace(reg3, (e) => {
                return e.replace(reg2, (e) => {
                    let tn = e.toLowerCase();
                    if (self._tags[tn])
                        return "this._plc._tags." + tn + "._value"
                    else
                        return e;
                });
            });
            for (let mc; mc = reg2.exec(v[4]);) {
                let tag = self._tags[mc[1].toLowerCase()];
                if (!tag) continue;
                tag._listener.push(v);
                tag.notify();
            }
        })
    }
    _collectListeners(log) {
        let encode = document.createElement("div");
        let addNodes = (nodes) => {
            for (let i = 0, l = nodes.length; i < l; i++) {
                let node = nodes[i];
                if (node.nodeType != 1) continue;
                if (node.nodeName == "BODY") continue;
                if (node["tagName"] && node.tagName == 'svg') {
                    function logchild(node) {
                        addNodes(node.childNodes);
                        for (let a = 0, b = node.childNodes.length; a < b; a++) {
                            logchild(node.childNodes[a]);
                        }
                    }
                    logchild(node);
                    continue;
                }
                if (node.mini) {
                    let mini = node.mini;
                    let id = mini.id;
                    /**
                     * @type {Array}
                     */
                    let taglist;
                    if (taglist = log.get(id)) {
                        taglist.forEach(el => {
                            let key = el[1].match(/style/i) ? el[1] : el[2];
                            key = key.replace(key.charAt(0), key.charAt(0).toUpperCase());
                            let setkey = 'set' + key;
                            if (!mini[setkey]) {
                                let idx = taglist.indexOf(el)
                                taglist.splice(idx, 1);
                            } else {
                                el[0] = mini;
                                el[1] = setkey;
                            }
                        });
                    } else {
                        taglist = [];
                    }
                    Object.keys(mini).forEach((key) => {
                        if (key.charAt(0) == '_') return;
                        key = key.replace(key.charAt(0), key.charAt(0).toUpperCase());
                        let setkey = 'set' + key;
                        let getkey = 'get' + key;
                        let value;
                        let mc;

                        if (mini[getkey] && mini[setkey] && (value = "" + mini[getkey]())) {
                            let reg1 = /([\w-]*) *?: *?\$(\w*)\$(\w*)[ ;"]?/g;
                            let reg2 = /(.*?\$(.*)\$.*)/;
                            reg1.lastIndex = 0;
                            let styled;
                            while (mc = reg1.exec(value)) {
                                taglist.push([mini, setkey, mc[1], mc[3], mc[2]]);
                                styled = true;
                            }
                            if (styled) return;
                            reg2.lastIndex = 0;
                            mc = reg2.exec(value);
                            if (mc)
                                taglist.push([mini, setkey, "", mc[1], mc[2]]);
                        }
                    })
                    if (!(this === PLC.prototype)) {
                        this.addListener(taglist);
                    } else {
                        if (taglist.length > 0)
                            log.set(id, taglist);
                        else
                            log.delete(id);
                    }

                } else {
                    let html = node.outerHTML;
                    let s
                    //id
                    let reg1 = / id="(\w*)"/;
                    //键值对
                    let reg2 = /(\w*?)="([^":]*?\$([\s\S\n]*?)\$[^:]*?)"/g;
                    //style
                    let reg5 = /(\w*?) *?= *?"((?: *?[\w-]*? *?:.*;?)+)"/;
                    let reg3 = / *?([\w-]*) *?: *?(\w*\$(.*)\$\w*)[ ;"]?/g;
                    //innerText;
                    let reg4 = /\$(.*)\$/g;
                    let smc;
                    let id;
                    if (smc = html.match(/^<.*? class=" *?mini-\w* *?".*?>/)) {
                        if (!(id = reg1.exec(smc[0]))) continue;
                        id = id[1];
                    } else if (!(smc = html.match(/^.*?>/))) {
                        continue;
                    } else {
                        id = node;
                    }
                    s = smc[0].replace(/(&quot;)|(&apos;)/g, "'");
                    reg2.lastIndex = 0;
                    let taglist = [];
                    let mp;
                    while (mp = reg2.exec(s)) {
                        if (mp[1] == "intenel") {
                            mp[2] = mp[2].replace(/&amp;/g, "&");
                            mp[2] = mp[2].replace(/(&gt&)|(&equ&)|(&br&)|(&bn&)/g, function (e) {
                                switch (e) {
                                    case "&gt&":
                                        return ">";
                                    case "&equ&":
                                        return "=";
                                    case "&br&":
                                        return "\r";
                                    case "&bn&":
                                        return "\n";
                                }
                            });
                        }
                        taglist.push([node, "setAttribute", mp[1], mp[2], mp[3]]);
                    }
                    let style = reg5.exec(s);
                    if (style) {
                        let stylename = style[1];
                        style = style[2];
                        reg3.lastIndex = 0;
                        while (mp = reg3.exec(style)) {
                            let idx = mp[1].indexOf('-');
                            if (idx >= 0) {
                                mp[1] = mp[1].replace(/\-./, mp[1].charAt(idx + 1).toUpperCase());
                            }
                            taglist.push([node, stylename, mp[1], mp[2], mp[3]]);
                        }
                        style = style.replace(reg3, "");
                        node.setAttribute('style', style);
                    }
                    if ((node.children.length == 0) && (mp = reg4.exec(node.innerText))) {
                        taglist.push([node, "", 'innerText', node.innerText, mp[1]]);
                    }
                    if ((node.children.length == 0) && (mp = reg4.exec(node.textContent))) {
                        taglist.push([node, "", 'textContent', node.textContent, mp[1]]);
                    }
                    if (taglist.length > 0) {
                        if (this === PLC.prototype) {
                            log.set(id, taglist);
                        } else {
                            this.addListener(taglist);
                        }
                    }
                }
            }
        }
        let removeNodes = (nodes) => {
            for (let i = 0, l = nodes.length; i < l; i++) {
                let node = nodes[i];
                if (node.nodeType != 1) continue;
                if (node.nodeName == 'BODY') continue;

                if (node.mini) {
                    let mini = node.mini;
                    let id = mini.id;
                    if (log.has(id)) {
                        log.delete(id);
                    }
                    if (mini['PLCTags'] && this !== PLC.prototype) {
                        for (let [key, value] of mini.PLCTags) {
                            this.removeListener(value, key);
                        }
                    }
                } else {
                    if (log.has(node)) {
                        log.delete(node);
                    }
                    if (node['PLCTags'] && this !== PLC.prototype) {
                        for (let [key, value] of node.PLCTags) {
                            this.removeListener(value, key);
                        }
                    }
                }
            }
        }
        let callback = function (mutations) {
            for (let i = 0, len = mutations.length; i < len; i++) {
                let mutation = mutations[i];
                if (mutation.type == 'childList') {
                    addNodes(mutation.addedNodes);
                    removeNodes(mutation.removedNodes);
                }
            }
        }

        function createDelegate(fn, scope) {
            let scop = scope;
            return function () {
                return fn.apply(scop, arguments);
            }
        }
        this.observer = new MutationObserver(createDelegate(callback, this));
        this.observer.observe(document, {
            childList: true,
            subtree: true
        });
    }
}

PLC.prototype._monitors = new Map();
PLC.prototype._collectListeners(PLC.prototype._monitors);